C++ 反射 第四章 标准
The following article is from CPP编程客 Author 里缪
到了此章,才算是正式进入「C++反射」的主题。
其实从第一章就已然确定了该系列的结构。鉴于「静态反射」还未进入标准,于是有了第二章T0层反射,这是当前项目中可以使用的;再有了第三章T1层反射,这是当前相对完整的反射实践,标准亦会参考Circle。
由于缺少底层机制,因此T0层反射几乎属于玩具级别,而Circle只供语法参考,无法在正式项目中使用。
这篇我们正式进入C++标准反射。
标准反射最早可于C++26/29进入标准,故本章几乎全是比较新的概念。
4.1 C++静态反射与元编程的关系
静态反射加入标准,将会使C++元编程进入一个全新的阶段。
为什么这样说呢?
在C++中,谈论元编程,一般我们是指编译期的编程。
其发展可以分为三个阶段。
第一个阶段,属于模板时期,起于C++98。
模板可以作为编译期计算的工具,对C++影响深远,借其可支持泛型编程,优化代码。缺点是需要对语言具有深入的理解,才能够使用模板来设计与维护代码。
第二个阶段,属于Constant Expressions(常量表达式)时期,起于C++11。
这使得编写编译期代码更加便捷,C++11开始,通过使用constexpr关键字,便可以令某些运算发生于编译期,C++20又添加了consteval与constinit,对编译期计算提供了更多支持。
第三个阶段,则属于静态反射时期,SG7仍在努力中。
大概从2010年,静态反射的研究工作便已启动,最终产生了一个TS版本(N4766),此时属于type-based反射。后来,出于种种原因,SG7转而支持之前就已提出的value-based反射(P0425r0)。
这个阶段将影响深远,极大增强C++元编程的能力,改变大家编程的方式。
本篇文章,就是带领大家进入第三阶段,学习value-based反射的最新成果。
需要提醒大家的是,静态反射本身强调的是反射能力,只有这种能力,根本不够。
因此,伴随静态反射还增加了一些其他提案,比如Expansion Statements用于遍历复数式反射元信息,「源码注入」用于支持更加强大的产生式元编程。
静态反射加上这些相关提案,才真正构成了反射大家族,这才是第三阶段的C++元编程。
4.2 实践环境的选择
只是纸上谈兵,社区自然没甚激情,所以SG7提供了一些基于reflection ts(后期value-based版本)的实现,以在社区激起一些浪花。
clang提供有一个支持reflection ts的版本,Compiler Explorer上可以直接使用。
然而这个版本不足以支持本篇内容,因为我们还需要Expansion Statements以及源码注入这些提案的支持,因此选择基于llvm的另一个版本:lock3。
lock3版本现在也没有维护了,但仍是当前最完善的实现版本,编译得半天,大家直接使用Compiler Explorer在线版本就可以,复制链接https://godbolt.org/z/nc34GKvMM 直接进入。
链接当中我已经提前写好了反射所需的全部头文件,这些头文件名称当然也不标准,一个实现一个样。
#include <experimental/meta>
#include <experimental/compiler>
using namespace std::experimental;
int main() {
}
反射属于元编程的第三个阶段,相关特性最终都会包含在<meta>头文件当中。关于该头文件的具体内容,后面有一节专门介绍。
完成了前置知识,下面开始正式进入标准反射的内容。
注意!本文讲解的内容基于最新的语法,而编译器的实现仍是较旧或有不符合提案的语法,因此同一个概念,讲解的语法与实际编写代码的语法,存在不一样的形式。各位要记住的是最新的语法,旧语法只是编写代码时不得不向编译器做出的妥协。
4.3 The ^ operator and Splicing
这两个概念在C++反射 第一章:通识中已经介绍过,对应reflection与reification。
reflection表示从类型得到「类型元信息」的这个动作,是从具体到抽象、自下而上的结构;而reification表示从「类型元信息」再次得到类型这个动作,是从抽象到具体、自上而下的结构。
这两个是反射通用的概念,在C++反射中reflection对应于“^ operator”,读作lifting operator,表示向上获取类型元对象;reification对应于splicing,语法为"[: refl :]",称为splice construct,表示重新具体化对象。
也就是说,"^ operator"是进入反射世界的钥匙,而“[: refl :]”则是回到现实世界的钥匙。
可以再次回到第一章的示例中熟悉一下这些概念。
#include <meta>
template<Enum T>
std::string to_string(T value) {
template for (constexpr auto e : std::meta::members_of(^T)) {
if([:e:] == value) {
return std::string(std::meta::name_of(e));
}
}
return "<unnamed>";
}
细节第一章讲了,不再赘述。
这个例子本身没什么用,主要是为了串联起诸多概念。
不过,这个例子当前编译不过,因为这两个概念的新语法当前没有任何编译器支持。
那么这个例子在lock3下如何编写呢?
代码如下:
1template<typename T>
2requires std::is_enum_v<T>
3consteval const char* enum_to_string(T value) {
4 template for (constexpr auto e : meta::members_of(reflexpr(T))) {
5 if(idexpr(e) == value) {
6 return __concatenate(meta::name_of(e));
7 }
8 }
9 return __concatenate("<unnamed>");
10}
11
12enum class Colors : unsigned char {
13 Red,
14 Green,
15 Blue,
16 Yellow,
17 Black
18};
19
20int main() {
21 constexpr const char* color_name = enum_to_string(Colors::Black);
22 constexpr auto __dummy = __reflect_print(color_name);
23}
为什么反射学起来很乱呢?就是因为概念之间变化太快了,一会叫这个,一会叫那个。
lock3当前实现的lifting operator还是最早使用的占位符:reflexpr()。而splicing的支持都是自己提供的,不像"[: ... :]"语法具有一致性,它提供了好几个操作符来支持不同的情况,idexpr()就是其中之一。
所有的操作都发生于编译期,而C++还不支持constexpr string(虽然可以借助std::string_view),但是lock3提供的__concatenate()用来产生编译期字符串要更加好用。此外,要在编译期输出constexpr string,lock3提供了__reflect_print()。
反射结果称为「元对象」(metaobject),也就是反射类型,定义为:
1namespace std::meta {
2 using info = decltype(^void);
3}
这是一个唯一的编译期常量值,所有的反射相关函数,参数都有meta::info。
通过lifting operator就能得到反射类型,再通过meta::members_of()来根据元对象获取类型的所有成员,它的返回值不止一个,若要遍历就得使用expansion statements。
运行上述程序,最终将会在编译期输出枚举值的字符形式,如图。
4.4 标准元编程库
标准元编程库,头文件为<meta>,其中的所有函数称为「元函数」(metafunctions)。
元编程库主要包含两部分内容,第一部分包含元编程的一些通用组件,第二部分包含反射TS中的元函数。
首先来看通用组件部分。
通用组件提供许多针对类型的工具,这些工具大多来自<type_traits>,只是将那些特性加到了value-based反射中来,以支持元对象(即反射类型meta::info)作为参数。
比如remove_const,定义为:
consteval info remove_const(info type) {
detail::require_type(type);
return __reflect(detail::query_remove_const, type);
}
那么如何使用呢?
其实和type_triats中一样,只是针对的是元对象,举个例子:
constexpr auto MyInt = meta::remove_const(reflexpr(const int));
typename(MyInt) var = 10;
var = 42;
这里通过lifting operator得到const int的元对象,再通过元函数移除该类型的const修饰,便得到了一个MyInt元对象。通过typename()可以将元对象splicing为一个类型,这代码就相当于int var = 10。
补充一下,上述splicing语法最新写法为typename[:MyInt:]。
因为<type_traits>大家基本都用过,只是将那些工具引入到反射里面,所以就不浪费篇幅介绍,以后的实际应用中再作解释。
接着来说第二部分,这些工具皆来自反射TS,都是新的元函数。
比如下面的例子,检测类成员权限:
1struct player {
2public:
3 float position_x;
4private:
5 float position_y;
6protected:
7 float position_z;
8};
9
10int main() {
11 constexpr bool pub = meta::is_public(reflexpr(player::position_x));
12 constexpr bool pri = meta::is_private(reflexpr(player::position_x));
13 constexpr bool pro = meta::is_protected(reflexpr(player::position_x));
14}
在反射之前,C++元编程并不具备这样的能力。
上面这种返回bool值的元函数,属于predicates一类,这个类别里面还有许多,列举一些如下:
1std::meta::is_unnamed
2std::meta::is_scoped_enum
3std::meta::is_declraed_constexpr
4std::meta::is_consteval
5std::meta::is_static
6std::meta::is_inline
7std::meta::is_deleted
8std::meta::is_defaulted
9std::meta::is_explicit
10std::meta::is_override
11std::meta::is_pure_virtual
12std::meta::is_class_member
13std::meta::is_local
14std::meta::is_namespace
15std::meta::is_template
16std::meta::is_type
17std::meta::is_incompltete_type
18std::meta::is_closure_type
19std::meta::has_captures
20std::meta::has_default_ref_capture
21std::meta::has_default_copy_capture
22std::meta::is_simple_capture
23std::meta::is_ref_capture
24std::meta::is_copy_capture
25std::meta::is_explicit_capture
26std::meta::is_init_capture
27std::meta::is_function_parameter
28std::meta::is_template_parameter
29std::meta::is_class_template
30std::meta::is_alias
31std::meta::is_alias_template
32std::meta::is_enumberator
33std::meta::is_variable
34std::meta::is_variable_template
35std::meta::is_static_data_member
36std::meta::is_nonstatic_data_member
37std::meta::is_bit_field
38std::meta::is_base_class
39std::meta::is_direct_base_class
40std::meta::is_virtual_base_class
41std::meta::is_function
42std::meta::is_function_template
43std::meta::is_member_function
44std::meta::is_member_function_template
45std::meta::is_static_member_function
46std::meta::is_static_member_function_template
47std::meta::is_nonstatic_member_function
48std::meta::is_nonstatic_member_function_template
49std::meta::is_constructor
50std::meta::is_constructor_template
51std::meta::is_destructor
52std::meta::is_destructor_template
53std::meta::is_lvalue
54std::meta::is_xvalue
55std::meta::is_prvalue
56std::meta::is_glvalue
57std::meta::is_rvalue
58std::meta::has_ellipsis
59std::meta::is_member_function_type
60std::meta::has_default
下面再来看元函数的另一个类别,单数形式。
意思很明显,此类元函数只会返回一个结果,比如前面使用过的meta::name_of()用来获取类型的名称。
这里再给个小例子,如何打印类型名称:
int a = 10;
constexpr auto r = meta::type_of(reflexpr(a));
constexpr auto n = meta::name_of(r);
std::cout << n; // output: int
这个类别也有一些元函数,不过lock3中实现的不多,列举一些如下:
1std::meta::source_location_of
2std::meta::name_of
3std::meta::display_name_of
4std::meta::entity
5std::meta::type_of
6std::meta::parent_of
7std::meta::current_function
8std::meta::current_class_type
9std::meta::byte_offset_of
10std::meta::bit_offset_of
11std::meta::byte_size_of
12std::meta::bit_size_of
13std::meta::this_ref_type
这里列举的很多元函数编译器目前不支持,大家注意一下。
最后来看元函数的另一个类别,复数形式。
顾名思义,就是返回多个结果的元函数。
这里举个打印类成员的例子:
1struct player {
2 static int x;
3 double y;
4private:
5 float z;
6protected:
7 void foo() {}
8};
9
10template<typename T>
11void print_members() {
12 constexpr auto members = meta::members_of(reflexpr(T));
13 template for (constexpr auto m : members) {
14 constexpr auto __dummy = __reflect_pretty_print(m);
15 }
16}
17
18int main() {
19 print_members<player>();
20}
在编译期将会输出:
static int x
double y
float z
void foo() {
}
打印所有成员,包括private与protected成员。这里meta::members_of()就是一个返回复数式反射信息的元函数,遍历这种元函数的结果,就需要使用expansion statements。
__reflect_pretty_print()是lock3提供的另一个编译期输出工具,参数为元对象,可以直接打印类型的实际声明形式。
meta::members_of()还有第二个参数,可以指定predicates一类的元函数,作为约束条件。比如:
// 获取所有非静态数据成员
meta::members_of(reflexpr(T), meta::is_nonstatic_data_member);
// 获取所有私有成员
meta::members_of(reflexpr(T), meta::is_private);
// 获取所有保护成员
meta::members_of(reflexpr(T), meta::is_protected);
同样,我们也可以获取函数的参数,代码如下:
1void foo(int a, double b, const std::string& c) {
2}
3
4void print_parameters() {
5 constexpr auto parameters = meta::parameters_of(reflexpr(foo));
6 template for (constexpr auto p : parameters) {
7 constexpr auto __dummy = __reflect_pretty_print(p);
8 }
9}
10
11int main() {
12 print_parameters();
13}
输出将为:
int a
double b
const std::string c
以上大概就是对<meta>库的一个整体介绍,下面将进入对于元编程来说,更加重要的一个模块:源码注入。
4.5 源码注入
什么是源码注入?为什么它如此重要呢?
我们使用反射,最主要的目的是使用「产生式元编程」,反射属于基本组件,让我们能够操纵「类型元信息」。而在此之上,需要一些其他特性,来支持使用反射进行产生式元编程。
源码注入,就是伴随反射而来的提案,使得我们可以编写产生代码的代码。
还是以一个简单的例子开始:
1struct X {
2 consteval {
3 for (int num = 0; num < 10; ++num)
4 -> fragment struct {
5 int unqualid("variable_", %{num});
6 };
7 }
8};
此处便使用源码注入,自动生成了以下代码:
1struct X {
2 int variable_0;
3 int variable_1;
4 int variable_2;
5 int variable_3;
6 int variable_4;
7 int variable_5;
8 int variable_6;
9 int variable_7;
10 int variable_8;
11 int variable_9;
12};
我们要进行注入的代码区域,称为metaprogram,语法如下:
consteval {
...
};
可以将metaprogram当作是一个不需要参数的consteval函数,编译时会自动执行。
metaprogram中有一些其他操作,比如例子中的for自动产生num,这些操作是不需要注入的,真正需要注入的语句,只要通过「注入语句」的语法"->"指定,就可以将这些代码注入到源码中去。
注入语句跟着的需要注入的代码片段称为fragments,是一个表达式,类型为元对象(meta::info)。
fragments有许多种类,比如namespace fragments, class fragments, enumeration fragments, block fragments,分别表示注入到不同的源码中去。例子中使用的fragment struct,就属于class fragments,也存在fragment class,区别与struct和class的区别一样。
注意,class fragments并不是描述一个真正的类,它仅仅是描述类中的成员。也就是说,不用给这个类起名字,起了也会被替换掉,最终被注入的只是这个类的body。class fragments只能注入到类里面。
那么如何在fragments中引入外部的变量呢?
这就需要使用unquote operator了,语法为"%{ ... }",它允许引用局部变量作为fragments中的变量名或类型名。
而unqualid operator,可以让我们从字符串组合新的代码。
前面说过,fragments的类型为元对象,那么其实是可以分开定义的,这是再举个fragment enumeration的例子:
constexpr meta::info rgb = fragment enum {
red, blue, green
};
enum class color {
consteval -> rgb
};
这里将会把fragments中的值注入到枚举的源码中去,metaprogram只有一行的情况下,就可以省略"{}"。
因此上面源码注入之后的代码为:
enum class color {
red,
blue,
green
};
配合反射,可以实现更多强大的源码注入能力,因为反射的类型也是元对象,所以可以直接注入。
看个简单的例子:
struct A {
void foo() {}
};
struct X {
consteval -> reflexpr(A::foo);
};
这样就轻松地将A的成员函数,注入到了X之中。
同时,也可以在注入的过程中,修改原始定义,比如:
1struct X {
2 consteval {
3 meta::info foo_refl = reflexpr(A::foo);
4 meta::make_constexpr(foo_refl);
5
6 const char* name = meta::name_of(foo_refl);
7 meta::set_new_name(foo_refl, __concatenate("constexpr_", name));
8
9 -> foo_refl;
10 }
11};
这就相当于如下声明:
struct X {
constexpr void constexpr_foo() {}
};
可以看到,反射配合源码注入,具备强大的产生式元编程能力。
关于源码注入,编译器支持的也不算全,本篇暂时只介绍这么多内容,更多内容以后再写。
以下各节,展示一些不太复杂的使用情境。
4.6 自动生成getters和setters
通过标准反射与源码注入,可以轻松为类成员实现getters和setters函数。
当前有如下类:
1struct book {
2 std::string title;
3 std::string author;
4 int page_count;
5
6 consteval {
7 gen_members(reflexpr(book));
8 }
9};
通过使用fragments,我们想自动生成如下代码:
1struct book {
2 std::string title;
3 std::string author;
4 int page_count;
5
6 std::string get_title() const {
7 return title;
8 }
9
10 void set_title(const std::string& title) {
11 this->title = title;
12 }
13
14 std::string get_author() const {
15 return author;
16 }
17
18 void set_author(const std::string& author) {
19 this->author = author;
20 }
21
22 int get_page_count() const {
23 return page_count;
24 }
25
26 void set_page_count(const int& page_count) {
27 this->page_count = page_count;
28 }
29};
那么首先,定义gen_members()函数,代码如下:
1consteval void gen_members(meta::info cls) {
2 auto members = meta::members_of(cls, meta::is_nonstatic_data_member);
3 for (meta::info member : members) {
4 gen_member(member);
5 }
6}
通过元函数遍历出类的所有非静态数据成员,再为每个数据成员生成getter和setter函数。
gen_member()函数的定义如下:
1consteval void gen_member(meta::info m) {
2 -> fragment struct {
3 typename(meta::type_of(%{m}))
4 unqualid("get_", meta::name_of(%{m}))() const {
5 return unqualid(meta::name_of(%{m}));
6 }
7 };
8
9 -> fragment struct {
10 void unqualid("set_", meta::name_of(%{m}))(const typename(meta::type_of(%{m}))& unqualid(meta::name_of(%{m}))) {
11 this->unqualid(meta::name_of(%{m})) = unqualid(meta::name_of(%{m}));
12 }
13 };
14}
通过定义两个fragments,利用元编程库中的组件,与源码注入的相关特性,就能够达到自动产生代码的能力。
这个fragments具备可复用性,对所有的类都适用。
这种对当前类生成一些新代码,有一种更推荐的方式,称为Metaclasses,这个概念属于源码注入。
上述例子以这种方式编写,代码如下:
1consteval void gen_members(meta::info cls) {
2 auto members = meta::members_of(cls, meta::is_nonstatic_data_member);
3 for (meta::info member : members) {
4 ->member;
5
6 gen_member(member);
7 }
8}
9
10struct(gen_members) book {
11 std::string title;
12 std::string author;
13 int page_count;
14};
Metaclasses就是一个metaprogram,不过它强调的是从一个类的原型(prototype)来为该类产生新的代码。
Metaclasses这种方式的语法为"struct(...)",这样就无需在类内编写metaprogram,这种方式要更加安全,编写的代码量也更少。
这里有一点需要注意,编译器在在实例化该定义时,会有一个隐藏的self元对象,还有一个处于匿名空间之中的book类,这个就是原型类。所有声明的成员处于原型类中,self当中没有,因此要使用"->member"将这些成员重新产生出来,否则book类当中找不到声明的那些成员。
此外,其实这个功能的实现,需要另一个特性配合才更好用,那就是「自定义Attributes」,存在相关提案,但是目前没法使用。
4.7 自动生成SQL语句
这个功能是第二章中出现的例子,现在更新为标准C++反射版本。
代码如下:
1consteval const char* to_sql(meta::info type) {
2 if(type == reflexpr(int))
3 return "INTEGER";
4 else if(type == reflexpr(std::string))
5 return "TEXT";
6
7 return "UNKNOWN_TYPE";
8}
9
10consteval const char* create_column(meta::info member) {
11 return __concatenate(meta::name_of(member),
12 " ", to_sql(meta::type_of(member)));
13}
14
15template<meta::info Class>
16consteval const char* create_table() {
17 const char* table_name = meta::name_of(Class);
18
19 const char* sql = __concatenate("CREATE TABLE ", table_name, "(");
20 bool first_seen = false;
21 for(meta::info member : meta::data_member_range(Class)) {
22 if(first_seen)
23 sql = __concatenate(sql, ", ");
24
25 sql = __concatenate(sql, create_column(member));
26 first_seen = true;
27 }
28 return __concatenate(sql, ");");
29}
代码逻辑第二章讲过,这里不再赘述。
测试代码,将会输出:
1struct person {
2 int age;
3 std::string name;
4};
5
6int main() {
7 std::cout << create_table<reflexpr(person)>();
8}
9
10// 输出:CREATE TABLE person(age INTEGER, name TEXT);
4.8 总结
还有其他一些例子当前编译器编译不了,本文就暂时先举这几个例子。
C++静态反射,核心内容本篇几乎全覆盖到了,重要的是要理解反射的基本语法与标准元编程库,以及源码注入。
静态反射和源码注入,对于产生式元编译来说,是非常强大的工具,能以此构建许多优秀的库或框架。
但是目前仍不完善,还在发展中,语法也没有完全统一,要进标准还得不少时间。
不过了解了这些概念,大家使用其他反射库应该不再是问题。
- EOF -
加主页君微信,不仅C/C++技能+1
主页君日常还会在个人微信分享C/C++开发学习资源和技术文章精选,不定期分享一些有意思的活动、岗位内推以及如何用技术做业余项目
加个微信,打开一扇窗
关注『CPP开发者』
看精选C++技术文章 . 加C++开发者专属圈子
点赞和在看就是最大的支持❤️